﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace DotNetToTypeScriptExporter
{
    public delegate void TypeProcessedDelegate(Type type);
    public delegate void TypeSkippedDelegate(Type type, bool notPublic, bool notClassOrInterface);
    
    public class DotNetToTypeScriptExporterException : Exception
    {
        public DotNetToTypeScriptExporterException(string message) : base(message) { }
        public DotNetToTypeScriptExporterException(string message, Exception innerException) : base(message) { }
    }
    
    public class DotNetToTypeScriptExporter
    {
        private IEnumerable<Assembly> _loadedAssemblies;
        private IEnumerable<Type> _typesToExport;
        
        private string _outputPath;
        private bool _exportFields;
        private bool _exportProperties;
        private bool _exportMethods;

        public event TypeProcessedDelegate OnTypeProcessed;
        public event TypeSkippedDelegate OnTypeSkipped;

        public void Export()
        {
            foreach (Type type in _typesToExport)
            {
                if (type.IsClass || type.IsInterface)
                    ExportType(type);
                else
                    OnTypeSkipped(type, !type.IsPublic, !(type.IsClass || type.IsInterface));
            }
        }
        
        private void ExportType(Type type)
        {
            string output = String.Empty;

            if (_exportFields)
            {
                foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
                {
                    string typeName = GetTypeName(field.FieldType);
                    output += "\t" + field.Name + (String.IsNullOrEmpty(typeName) ? "" : " : " + typeName) + ";\r\n";
                }

                if (!String.IsNullOrEmpty(output)) output += "\r\n";
            }

            if (_exportProperties)
            {
                List<string> _propertiesDone = new List<string>();
                foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
                {
                    string propertyName = property.Name; // indexers can be overloaded
                    int suffix = 1;
                    while (_propertiesDone.Contains(propertyName))
                        propertyName = property.Name + "_overload_" + suffix++.ToString();

                    string typeName = GetTypeName(property.PropertyType);
                    output += "\t" + propertyName + (String.IsNullOrEmpty(typeName) ? "" : " : " + typeName) + ";\r\n";

                    _propertiesDone.Add(propertyName);
                }

                if (!String.IsNullOrEmpty(output)) output += "\r\n";
            }

            if (_exportMethods && !type.IsInterface)
            {
                List<string> _methodsDone = new List<string>();
                foreach (MethodInfo method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(m => !m.Name.StartsWith("get_") && !m.Name.StartsWith("set_")))
                {
                    string methodName = method.Name; // may have to suffix this for overloads
                    int suffix = 1;
                    while (_methodsDone.Contains(methodName))
                        methodName = method.Name + "_overload_" + suffix++.ToString();

                    string returnTypeName = GetTypeName(method.ReturnType);
                    if (returnTypeName.ToLower() == "void") returnTypeName = String.Empty;

                    output += "\t" + methodName + "(";
                    foreach (ParameterInfo parameter in method.GetParameters())
                    {
                        string parameterTypeName = GetTypeName(parameter.ParameterType);
                        output += parameter.Name + (String.IsNullOrEmpty(parameterTypeName) ? "" : " : " + parameterTypeName) + ", ";
                    }
                    output = output.TrimEnd(',', ' ') + ")" + (String.IsNullOrEmpty(returnTypeName) ? "" : " : " + returnTypeName) + " {\r\n\t\t//method body goes here" +
                        (String.IsNullOrEmpty(returnTypeName) ? String.Empty : "\r\n\t\treturn null;") + "\r\n\t}\r\n\r\n";

                    _methodsDone.Add(methodName);
                }
            }

            output = output.TrimEnd('\r', '\n');
            output += "\r\n";
            output += "}\r\n\r\n";

            output = (type.IsClass ? "class " : "interface ") + (type.IsGenericType ? type.Name.Substring(0, type.Name.IndexOf('`')) : type.Name) + " {\r\n" + output;

            using (StreamWriter writer = File.AppendText(_outputPath))
                writer.Write(output);

            OnTypeProcessed(type);
        }

        private string GetTypeName(Type type)
        {
            if (type == null)
            {
                return String.Empty;
            }

            bool isArray = false;
            string output = String.Empty;

            // special cases

            // Nullable types
            type = (Nullable.GetUnderlyingType(type)) ?? type;
            
            // Array types
            if (type.HasElementType)
            {
                isArray = true;
                type = type.GetElementType();
            }

            // Enumerables
            // IEnumerable<T>
            if (typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType)
            {
                isArray = true;
                if (type.GetGenericTypeDefinition() != typeof(IEnumerable<>))
                {
                    type = type.GetInterfaces().Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)).GetGenericArguments()[0];
                }
                else // special case - this is actually IEnumerable<T> itself
                {
                    type = type.GetGenericArguments()[0];
                }
            }

            // Enum types - carry names, not values
            if (type.IsEnum)
                output += "string";
            
            // Primitive types
            switch (type.FullName)
            {
                case "System.String":
                case "System.Guid":
                case "System.DateTime":
                    output += "string";
                    break;

                case "System.Int32":
                case "System.Int64":
                case "System.Int16":
                case "System.UInt32":
                case "System.UInt64":
                case "System.UInt16":
                case "System.Decimal":
                case "System.Float":
                    output += "number";
                    break;

                case "System.Boolean":
                    output += "bool";
                    break;

                default:
                    break;
            }

            if (output == String.Empty)
            {
                string typeName = type.Name;
                // if type is generic, remove generic wart from name
                if (typeName.IndexOf('`') >= 0)
                    typeName = typeName.Substring(0, type.Name.IndexOf('`'));
                
                // we only allow types that are defined in the ts file or primitives
                if (_typesToExport.Contains(type))
                    output += typeName;
            }

            if (isArray && !String.IsNullOrEmpty(output))
                output += "[]";

            return output;
        }
                
        private void TryCreateOutput()
        {
            string folder = Path.GetDirectoryName(_outputPath);
            if (folder != String.Empty && !Directory.Exists(folder))
                throw new DirectoryNotFoundException("Specified output folder does not exist.");

            if (File.Exists(_outputPath)) File.Delete(_outputPath);
            File.OpenWrite(_outputPath).Close(); // creates empty file or exception if error
        }

        public DotNetToTypeScriptExporter(string outputPath, IEnumerable<string> assemblyPaths, IEnumerable<string> typeNames, bool ignoreTypeNameCase, bool exportFields, bool exportProperties, bool exportMethods)
        {
            _outputPath = outputPath;
            _exportFields = exportFields;
            _exportProperties = exportProperties;
            _exportMethods = exportMethods;
            List<Assembly> assemblyList = new List<Assembly>();

            try
            {
                foreach (string assemblyPath in assemblyPaths)
                {
                    Assembly assembly = Assembly.LoadFile(assemblyPath);
                    assemblyList.Add(assembly);
                }
                
                _loadedAssemblies = assemblyList;
            }
            catch (Exception ex)
            {
                throw new DotNetToTypeScriptExporterException("Error loading assemblies - " + ex.Message, ex);
            }

            if (typeNames != null && typeNames.Count() > 0)
            {
                string[] names = typeNames.ToArray();
                if (ignoreTypeNameCase)
                    for (int i = 0; i < names.Count(); i++)
                        names[i] = names[i].ToLower();

                try
                {
                    _typesToExport = new List<Type>(_loadedAssemblies.SelectMany(a => a.GetTypes()).Where(t => names.Contains(t.FullName.ToLower()) && (t.IsClass || t.IsInterface)));
                }
                catch (Exception ex)
                {
                    throw new DotNetToTypeScriptExporterException("Error loading types - " + ex.Message, ex);
                }
            }
            else
            {
                _typesToExport = new List<Type>(_loadedAssemblies.SelectMany(a => a.GetTypes()).Where(t => t.IsPublic && (t.IsClass || t.IsInterface)));
            }

            TryCreateOutput();
        }
    }
}
